/**
*    Copyright (C) 2008  Chase Kernan 
*    chase.kernan@gmail.com
*
*    This program is free software: you can redistribute it and/or modify
*    it under the terms of the GNU General Public License as published by
*    the Free Software Foundation, either version 3 of the License, or
*    (at your option) any later version.
*
*    This program is distributed in the hope that it will be useful,
*    but WITHOUT ANY WARRANTY; without even the implied warranty of
*    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
*    GNU General Public License for more details.
*
*    You should have received a copy of the GNU General Public License
*    along with this program.  If not, see <http://www.gnu.org/licenses/>.
* 
**/

package com.chasekernan.hxnova.core.races;

import com.chasekernan.hxnova.core.races.PRTSet;
import com.chasekernan.hxnova.core.races.LRTSet;
import haxe.xml.Check;
import com.chasekernan.hxnova.core.races.GeneratePoints;
import com.chasekernan.hxnova.utils.Utils;

/**
    Each player has a predefined race that determines many of its technological
    and economic advantages/disadvantages.
**/
class Race {
    
    //these values are the basic limits on some race options.
    public static var NAME_LENGTH_MAX  : Int = 16;
    public static var PLURAL_NAME_LENGTH_MAX : Int = 20;
    public static var GROWTH_RATE_MIN : Int = 1;
    public static var GROWTH_RATE_MAX : Int = 20;
    public static var POP_PRODUCTION_MIN : Int = 700;
    public static var POP_PRODUCTION_MAX : Int = 2500;
    public static var FACTORY_PRODUCTION_MIN : Int = 5;
    public static var FACTORY_PRODUCTION_MAX : Int = 25;
    public static var FACTORY_COST_MIN : Int = 5;
    public static var FACTORY_COST_MAX : Int = 25;
    public static var OPERABLE_FACTORIES_MIN : Int = 5;
    public static var OPERABLE_FACTORIES_MAX : Int = 25;
    public static var MINE_PRODUCTION_MIN : Int = 5;
    public static var MINE_PRODUCTION_MAX : Int = 25;
    public static var MINE_COST_MIN : Int = 2;
    public static var MINE_COST_MAX : Int = 15;
    public static var OPERABLE_MINES_MIN : Int = 5;
    public static var OPERABLE_MINES_MAX : Int = 25;
    
    public static var MINERAL_SCRAPPING : Float = 0.6;
    public static var RESOURCE_SCRAPPING : Float = 0.0;
    
    //the following functions are just checks to make sure the above limits are 
    //followed.
    public var name(default, setName) : String;
    private function setName(name : String) {
        if(name.length <= NAME_LENGTH_MAX) this.name = name;
        else throw "Name is too long.";
        return this.name;
    }
    
    public var pluralName(default, setPluralName) : String;
    private function setPluralName(name : String) {
        if(name.length <= PLURAL_NAME_LENGTH_MAX) this.pluralName = name;
        else throw "Plural name is too long.";
        return this.name;
    }
    
    public var growthRate(default, setGrowthRate) : Int;
    private function setGrowthRate(v : Int){return setValue("growthRate", v);}
    
    public var popProduction(default, setPopProduction) : Int;
    private function setPopProduction(v : Int)
            {return setValue("popProduction", v);}
    
    public var factoryProduction(default, setFactoryProduction) : Int;
    private function setFactoryProduction(v : Int)
            {return setValue("factoryProduction", v);}
    
    public var factoryCost(default, setFactoryCost) : Int;
    private function setFactoryCost(v : Int){return setValue("factoryCost", v);}
    
    public var operableFactories(default, setOperableFactories) : Int;
    private function setOperableFactories(v : Int)
            {return setValue("operableFactories", v);}
    
    public var mineProduction(default, setMineProduction) : Int;
    private function setMineProduction(v : Int)
            {return setValue("mineProduction", v);}
    
    public var mineCost(default, setMineCost) : Int;
    private function setMineCost(v : Int){return setValue("mineCost", v);}
    
    public var operableMines(default, setOperableMines) : Int;
    private function setOperableMines(v : Int)
            {return setValue("operableMines", v);}
    
    private static function capAndUnderline(s : String) : String {
        var newStart = 0;
        var newString = "";
        for(c in 0...s.length) {
            if(s.charAt(c) == s.charAt(c).toUpperCase()) {
                newString += s.substr(newStart, c - newStart).toUpperCase() 
                                + "_";
                newStart = c;
            }
        }
        newString += s.substr(newStart).toUpperCase();
        return newString;
    }
    
    //this is kind of a hack function that restricts a field to the defined
    //limits using some reflection.
    private function setValue(field : String, value : Int) : Int {
        var max : Int = cast(Reflect.field(Race, capAndUnderline(field) + 
                "_MAX"), Int);
        var min : Int = cast(Reflect.field(Race, capAndUnderline(field) + 
                "_MIN"), Int);
                
        if(value < min || value > max)
            throw "Value for " + field + " is out of range.";
        else Reflect.setField(this, field, value);
        
        var newValue : Int = cast(Reflect.field(this, field), Int);
        return newValue;
    }
    
    public var factoriesCostLessMin : Bool;
    public var prt : PRT;
    public var lrts : Array<LRT>;
    public var habSetting : HabSetting;
    public var researchCosts : ResearchCosts;
    public var startsWithExtraTech : Bool;
    public var startingTech(getStartingTech, null) : TechLevel;
    
    private function getStartingTech() : TechLevel {
        var startingLevel = 0;
        if (startsWithExtraTech) startingLevel += switch (prt) {
            case JackOfAllTrades: 4;
            default: 3;
        }
        
        var tech = new TechLevel(startingLevel);
        
        //add lrts bonuses here
        if (hasLRT(ImprovedFuelEfficiency)) tech.increaseLevel("propulsion");
        
        return tech;
    }
    
    //TODO: Actually make an external file of race defaults.
    
    /**
        All of the values are set to the defaults provided in an external file.
    **/
    public function new() {
        //these defaults should be loaded from an external file
        name = "Name";
        pluralName = "Plural Name";
        prt = PRTSet.PRT_LIST[0];
        lrts = new Array();
        habSetting = new HabSetting();
        growthRate = 15;
        popProduction = 1000;
        factoryProduction = 10;
        factoryCost = 10;
        operableFactories = 10;
        factoriesCostLessMin = false;
        mineProduction = 10;
        mineCost = 5;
        operableMines = 10;
        researchCosts = new ResearchCosts();
        startsWithExtraTech = false;
        
    }
    
    /**
        Returns true if the race has a certain lrt selected.
    **/
    public function hasLRT(lrt : LRT) : Bool {
        for (i in lrts) { if (i == lrt) return true; }
        return false;
    }
    
    /**
        Make the race have no lrts enabled.
    **/
    public function clearLRTs() {
        lrts = new Array();
    }
    
    /**
        Returns the percent of resources/minerals gained by scrapping a fleet.
    **/
    public function getScrap() : { resources : Float, minerals : Float } {
        return { resources : RESOURCE_SCRAPPING, minerals : MINERAL_SCRAPPING };
    }
    
    /**
        Returns the number of advantage points of the race (calculated by
        GeneratedPoints class.
    **/
    public function getPoints() : Int {
        return GeneratePoints.generate(this);
    }
    
    public static var XML_TITLE = "race";
    
    /**
        Writes a race to xml.
        Sample XML:
            <race>
                <name>Name</name>
                <plural_name>Plural Name</plural_name>
                <prt>HyperExpansion</prt>
                <!-- lrts are stored like prt, except separated by a comma -->
                <lrts/>
                <hab_setting>
                    <gravity lower_bound="15" upper_bound="85"/>
                    <temperature lower_bound="15" upper_bound="85"/>
                    <radiation lower_bound="15" upper_bound="85"/>
                </hab_setting>
                <growth_rate>15</growth_rate>
                <pop_production>1000</pop_production>
                <factory_production>10</factory_production>
                <factory_cost>10</factory_cost>
                <operable_factories>10</operable_factories>
                <factories_cost_less_min>false</factories_cost_less_min>
                <mine_production>10</mine_production>
                <mine_cost>5</mine_cost>
                <operable_mines>10</operable_mines>
                <research_costs>
                    <energy value="1"/>
                    <weapons value="1"/>
                    <propulsion value="1"/>
                    <construction value="1"/>
                    <electronics value="1"/>
                    <biotechnology value="1"/>
                </research_costs>
                <stars_width_extra_tech>false</stars_width_extra_tech>
                <points>1528</points>
            </race>
    **/
    public static function writeToXml(r : Race) : Xml {
        var xml = Xml.parse('<' + XML_TITLE + '></' + XML_TITLE + '>')
                .firstChild();
        
        addXmlChild(xml, 'name', r.name);
        addXmlChild(xml, 'plural_name', r.pluralName);
        addXmlChild(xml, 'prt', r.prt);
        
        var lrtString = if(r.lrts.length >= 1) Std.string(r.lrts[0]) else '';
        for(i in 1...r.lrts.length) lrtString += ', '+Std.string(r.lrts[i]);
        addXmlChild(xml, 'lrts', lrtString);
        
        xml.addChild(HabSetting.writeToXml(r.habSetting));
        
        addXmlChild(xml, 'growth_rate', r.growthRate);
        addXmlChild(xml, 'pop_production', r.popProduction);
        addXmlChild(xml, 'factory_production', r.factoryProduction);
        addXmlChild(xml, 'factory_cost', r.factoryCost);
        addXmlChild(xml, 'operable_factories', r.operableFactories);
        addXmlChild(xml, 'factories_cost_less_min', r.factoriesCostLessMin);
        addXmlChild(xml, 'mine_production', r.mineProduction);
        addXmlChild(xml, 'mine_cost', r.mineCost);
        addXmlChild(xml, 'operable_mines', r.operableMines);
        
        xml.addChild(ResearchCosts.writeToXml(r.researchCosts));
        
        addXmlChild(xml, 'stars_width_extra_tech', r.startsWithExtraTech);
        
        addXmlChild(xml, 'points', r.getPoints());
        
        return xml;
    }
    
    //used to add basic data
    private static function addXmlChild(xml : Xml, name : String, 
            value : Dynamic) {
        xml.addChild(Xml.parse('<'+name+'>'+Std.string(value)+'</'+name+'>')
                .firstChild());
    }
    
    /**
        Reads from a race xml file. Throws an error if there are any 
        mistakes.
    **/
    public static function readFromXml(xml : Xml) : Race {
        try {
            
            var r = new Race();
            
            for(i in xml.elements()) {
                switch(i.nodeName) {
                    case 'name': 
                        r.setName(i.firstChild().nodeValue);
                    
                    case 'plural_name': 
                        r.setPluralName(i.firstChild().nodeValue);
                    
                    case 'prt': 
                        r.prt = PRTSet.getPRTByName(i.firstChild().nodeValue);
                    
                    case 'lrts': 
                        r.lrts = new Array<LRT>();
                        
                        if (i.firstChild().nodeValue.length >= 1) {
                            var list = i.firstChild().nodeValue.split(', ');
                            for(j in list) r.lrts.push(LRTSet.getLRTByName(j));
                        }
                        
                    case HabSetting.XML_TITLE:
                        r.habSetting = HabSetting.readFromXml(i);
                    
                    case 'growth_rate': r
                        .setGrowthRate(Std.parseInt(i.firstChild().nodeValue));
                    
                    case 'pop_production': r
                        .setPopProduction(Std.parseInt(i.firstChild()
                                .nodeValue));
                    
                    case 'factory_production': r
                        .setFactoryProduction(Std.parseInt(i.firstChild()
                                .nodeValue));
                    
                    case 'factory_cost': r
                        .setFactoryCost(Std.parseInt(i.firstChild().nodeValue));
                    
                    case 'operable_factories': 
                        r.setOperableFactories(Std.parseInt(i.firstChild()
                                .nodeValue));
                    
                    case 'factories_cost_less_min': 
                        r.factoriesCostLessMin = 
                                i.firstChild().nodeValue == 'true';
                    
                    case 'mine_production': 
                        r.setMineProduction(Std.parseInt(i.firstChild()
                                .nodeValue));
                    
                    case 'mine_cost': 
                        r.setMineCost(Std.parseInt(i.firstChild().nodeValue));
                    
                    case 'operable_mines': 
                        r.setOperableMines(Std.parseInt(i.firstChild()
                                .nodeValue));
                    
                    case ResearchCosts.XML_TITLE: 
                        r.researchCosts = ResearchCosts.readFromXml(i);
                        
                    case 'stars_width_extra_tech':
                        r.startsWithExtraTech = 
                                i.firstChild().nodeValue == 'true';
                        
                    case 'points':
                        //check later
                        
                    default:
                        throw "Unrecognized xml element: " + i.nodeName;
                }
            }
            
            var points = Std.parseInt(Utils.getXmlElement(xml, "points")
                    .firstChild().nodeValue);
                    
            //check to make sure points match up
            if(r.getPoints() != points)
                throw "Points do not match: " + r.getPoints() + " "+points;
                
            return r;
            
        } catch (e : Dynamic) {
            throw "Bad race file: " + e;
        }
    }
}